/*
 *  Copyright (C) 2011  Clint Priest
 *
 *  This file is part of pklib.
 *
 *  pklib is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  pklib is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with pklib.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

var UserElementLasso = Class.create({ 
	X1: 0, Y1: 0, X2: 0, Y2: 0,
	
	/* All unfiltered elements which are outlined by user */
	IntersectedElements: 	[ ],
	/* All intersected elements outlined by user which were not filtered out by .FilterIntersectedElements() */
	SelectedElements: 		[ ],
	
	/* Default style to apply to selected elements */
	SelectedElementsStyle:	{ MozOutline: '1px solid red', },
	
	/* Default style for lasso */
	LassoElementStyle: 		{ border: '3px dashed #008800', },
	
	SelectableElementSelectors:	[ /* Array of CSS Selectors to identify selectable elements */ ],
	
	/* Event parameters that must match to activate lasso */
	MouseDownActivateParams: { button: MouseButtons.Right, ctrlKey: false, altKey: false, shiftKey: false },
	
	/* Returns a rect whos X1,Y1 are the closest to zero of the four corners */
	NormalizedRect: {
		get: function() { 
			return {	X1: Math.min(this.X1,this.X2),	Y1: Math.min(this.Y1,this.Y2),
						X2: Math.max(this.X1,this.X2),	Y2: Math.max(this.Y1,this.Y2)	};
		}
	},
		
	/* Behavioral Options */
	Options:	{
		/* Flag to hide the selection if the mouse leaves the browser window, presently disabled */
		HideSelectionOnMouseLeave: 	false,
	},
		
	/* Flag indicating that a selection has been started */
	Dragging: false,
	
	initialize: function() {
		this.PanelContainer = document.getElementById('content').mPanelContainer;
		this.PanelContainer.addEventListener('mousedown', this.OnMouseDown.bind(this), false);
		this.PanelContainer.addEventListener('mouseup', this.OnMouseUp.bind(this), true);
		
		window.addEventListener('resize', this.OnWindowResize.bind(this), true);
		
		this._OnMouseMove 	= this.OnMouseMove.bind(this);
	},
	
	/* Element bounding rectangles are calculated at the start, if the window is resized during a drag, then we recalculate */
	OnWindowResize: function(e) {
		if(this.Dragging == true)
			this.CalculateSnapRects();
	},
	
	/* Returns true if all paramters of this.MouseDownActivateParams match event params */
	ShouldActivate: function(e) {
		return !Object.keys(this.MouseDownActivateParams).some( function(param) {
			return e[param] != this.MouseDownActivateParams[param];
		}, this);
	},
	
	/* Starting Hook for beginning a selection */
	OnMouseDown: function(e) {
		if(!this.ShouldActivate(e))
			return;

		this.Document = e.target.ownerDocument;
		this.Document.body.setCapture(false);

		/** Initializes the starting mouse position */
		this.X1 = this.X2 = Math.min(e.pageX,this.Document.documentElement.offsetWidth + this.Document.defaultView.pageXOffset);
		this.Y1 = this.Y2 = e.pageY;

		this.PanelContainer.addEventListener('mousemove', this._OnMouseMove, true);
	},
	
	OnMouseMove: function(e) {
		if(this.Element) {
			if((e.clientX < 0 || e.clientY < 0 || e.clientX > this.PanelContainer.clientWidth || e.clientY > this.PanelContainer.clientHeight) && this.Options.HideSelectionOnMouseLeave)
				this.Element.style.display = 'none';
			else
				this.Element.style.display = '';
		}

		this.ExpandSelectionTo(e.pageX, e.pageY);
	},
	
	OnMouseUp: function(e) {
		if(this.Dragging) {
			this.Document.releaseCapture(false);

			var evt = document.createEvent('Event');
			evt.initEvent('UserElementLasso:SelectionMade', true, true);
			evt.SelectedElements = this.SelectedElements;

			this.RemoveEventHooks();
			this.Clear();

			this.Document.body.dispatchEvent(evt);
		}
	},
	
	/* Creates the selection rectangle element, returns true if element exists or was created successfully */
	Create: function() {
		if(this.Dragging == true)
			return true;
			
		if(Math.abs(this.X1-this.X2) > 4 || Math.abs(this.Y1-this.Y2) > 4) {
			var InsertionNode = (this.Document.documentElement) ? this.Document.documentElement : this.Document;
			
			this.Element = $(this.Document.createElementNS('http://www.w3.org/1999/xhtml', 'UserElementLasso'));
			if(InsertionNode && this.Element) {
				this.Element.ApplyStyle({ position: 'absolute', zIndex: '10000', left: this.X1 + 'px', top: this.Y1 + 'px' } );
				this.Element.ApplyStyle(this.LassoElementStyle);
				InsertionNode.appendChild(this.Element);

				this.Dragging = this.Element.parentNode != undefined;

				if(this.Dragging) {
					this.CalculateSnapRects();
					return true;
				}
			}
		}
		return false;
	},
	
	/** Calculates and caches the rectangles that make up all document lengths */
	CalculateSnapRects: function() {
		/* If the last calculation was done at the same innerWidth, skip calculation */
		if(this.CalculateWindowWidth == window.innerWidth)
			return;
			
		this.CalculateWindowWidth = window.innerWidth;
		var SelectableElements = [ ];

//		var Start = (new Date()).getMilliseconds(), Last = Start;
		
		this.SelectableElementSelectors.forEach( function(CssPath) {
			var Nodes = $($A(this.Document.body.querySelectorAll(CssPath)));
			SelectableElements = SelectableElements.concat(Nodes);
		}, this);

		var offset = { x: this.Document.defaultView.scrollX, y: this.Document.defaultView.scrollY };

		SelectableElements.forEach( function(elem) {
			elem.SnapRects = elem.GetOffsetElementRects(offset);
		}, this );

		this.SelectableElements = SelectableElements;

//		var End = (new Date()).getMilliseconds();
//		console.log("CalculateSnapRects: %sms", Math.round(End - Start, 2));
	},

	/** Clears the selection by removing the element, also clears some other non-refactored but moved code */
	Clear: function() {
		if (this.Element)
			this.Element.parentNode.removeChild(this.Element);
		delete this.Element;
		
		this.ClearSelectedElements();
		
		this.X1=0; this.X2=0; this.Y1=0; this.Y2=0;
		this.Dragging = false;
		this.RemoveEventHooks();
		
		delete this.CalculateWindowWidth;
	},
	/* Clears the selection style from the currently selected elements */
	ClearSelectedElements: function() {
		this.IntersectedElements = [ ];

		this.SelectedElements.forEach( function(elem) {
			(elem.SnapOutlines || [ elem ]).forEach( function(elem) {
				elem.ApplyStyle(elem.SGOriginalStyle || { });
				delete elem.SGOriginalStyle;
			} );
		} );
		this.SelectedElements = [ ];
	},

	/* Cleans up the hooks that were started during mouse down */
	RemoveEventHooks: function() {
		this.PanelContainer.removeEventListener('mousemove', this._OnMouseMove, true);
	},

	/** Offsets the selection by the given coordinates */
	OffsetSelection: function(X, Y) {
		this.X1 += X;	this.Y1 += Y;
		this.X2 += X;	this.Y2 += Y;
		this.UpdateElement();
	},
	
	/* Expands the selection to the given X2, Y2 coordinates */
	ExpandSelectionTo: function(X, Y) {
		this.X2 = X;	this.Y2 = Y;
		this.UpdateElement();
	},
	
	/* Updates the visible position of the element */
	UpdateElement: function() {
		if(this.Create()) {
			var BorderWidth = parseInt(this.LassoElementStyle.border);
			this.Element.ApplyStyle({
				width 	: Math.abs(this.X1-this.X2) - BorderWidth + 'px',
				height 	: Math.abs(this.Y1-this.Y2) - BorderWidth + 'px',
				top 	: Math.min(this.Y1,this.Y2) - BorderWidth + 'px',
				left 	: Math.min(this.X1,this.X2) - BorderWidth + 'px'
			} );
			
			this.CalcSelectedElements();
		}
	},
	
	/* Calculates which elements intersect with the selection */
	CalcSelectedElements: function() {
		this.ClearSelectedElements();
		if(this.Element.style.display != 'none') {
			var SelectRect = this.NormalizedRect;
			var IntersectedElements = [ ];
			
			/* Find Links Which Intersect With Selection Rectangle */
			$A(this.SelectableElements).forEach( function( elem ) {
				var Intersects = elem.SnapRects.some( function(Rect) {
					return !( SelectRect.X1 > Rect.right || SelectRect.X2 < Rect.left || SelectRect.Y1 > Rect.bottom || SelectRect.Y2 < Rect.top );
				});
				
				if(Intersects)
					IntersectedElements.push(elem);
			});
			
			var SelectedElements = IntersectedElements.filter( this.FilterIntersectedElements.bind(this) );
			
			this.HighlightSelectedElements(SelectedElements);
			
			this.IntersectedElements = IntersectedElements;
			this.SelectedElements = SelectedElements;
		}
	},
	/* Over-ride to filter intersected elements to selected elements */
	FilterIntersectedElements: function() {	return true; },
	
	/* Default implementation to show which elements have been selected */
	HighlightSelectedElements: function(SelectedElements) {
		SelectedElements.forEach( function(elem) {
			(elem.SnapOutlines || [ elem ]).forEach( function(elem) {
				elem.SGOriginalStyle = elem.ApplyStyle(this.SelectedElementsStyle);
			}, this );
		}, this );
	}
} );